/**
 * Licensed under the Artistic License; you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * 
 * http://displaytag.sourceforge.net/license.html
 * 
 * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

package org.displaytag.tags;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.JspWriter;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.LongRange;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.math.Range;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.displaytag.Messages;
import org.displaytag.decorator.TableDecorator;
import org.displaytag.exception.ExportException;
import org.displaytag.exception.FactoryInstantiationException;
import org.displaytag.exception.InvalidTagAttributeValueException;
import org.displaytag.exception.WrappedRuntimeException;
import org.displaytag.export.BinaryExportView;
import org.displaytag.export.ExportView;
import org.displaytag.export.ExportViewFactory;
import org.displaytag.export.TextExportView;
import org.displaytag.model.Cell;
import org.displaytag.model.Column;
import org.displaytag.model.HeaderCell;
import org.displaytag.model.Row;
import org.displaytag.model.TableModel;
import org.displaytag.pagination.PaginatedList;
import org.displaytag.pagination.PaginatedListSmartListHelper;
import org.displaytag.pagination.SmartListHelper;
import org.displaytag.properties.MediaTypeEnum;
import org.displaytag.properties.SortOrderEnum;
import org.displaytag.properties.TableProperties;
import org.displaytag.render.HtmlTableWriter;
import org.displaytag.util.CollectionUtil;
import org.displaytag.util.DependencyChecker;
import org.displaytag.util.Href;
import org.displaytag.util.ParamEncoder;
import org.displaytag.util.RequestHelper;
import org.displaytag.util.RequestHelperFactory;
import org.displaytag.util.TagConstants;

/**
 * This tag takes a list of objects and creates a table to display those
 * objects. With the help of column tags, you simply provide the name of
 * properties (get Methods) that are called against the objects in your list
 * that gets displayed. This tag works very much like the struts iterator tag,
 * most of the attributes have the same name and functionality as the struts
 * tag.
 * 
 * @author mraible
 * @author Fabrizio Giustina
 * @version $Revision: 1025 $ ($Author: fgiust $)
 */
public class TableTag extends HtmlTableTag {

	/**
	 * If this variable is found in the request, assume the export filter is
	 * enabled.
	 */
	public static final String			FILTER_CONTENT_OVERRIDE_BODY	= //
																		"org.displaytag.filter.ResponseOverrideFilter.CONTENT_OVERRIDE_BODY";	//$NON-NLS-1$

	/**
	 * logger.
	 */
	private static Log					log								= LogFactory
																				.getLog(TableTag.class);

	/**
	 * name of the attribute added to page scope when exporting, containing an
	 * MediaTypeEnum this can be used in column content to detect the output
	 * type and to return different data when exporting.
	 */
	public static final String			PAGE_ATTRIBUTE_MEDIA			= "mediaType";															//$NON-NLS-1$

	/**
	 * RequestHelperFactory instance used for link generation.
	 */
	private static RequestHelperFactory	rhf;

	/**
	 * D1597A17A6.
	 */
	private static final long			serialVersionUID				= 899149338534L;

	/**
	 * base href used for links - set in initParameters().
	 */
	private Href						baseHref;

	// -- start tag attributes --

	/**
	 * Static caption added using the footer tag.
	 */
	private String						caption;

	/**
	 * Child caption tag.
	 */
	private CaptionTag					captionTag;

	/**
	 * export type - set in initParameters().
	 */
	private MediaTypeEnum				currentMediaType;

	/**
	 * current row.
	 */
	private Row							currentRow;

	/**
	 * table decorator class name.
	 */
	private String						decoratorName;

	/**
	 * the index of the column sorted by default.
	 */
	private int							defaultSortedColumn				= -1;

	/**
	 * the sorting order for the sorted column.
	 */
	private SortOrderEnum				defaultSortOrder;

	/**
	 * daAfterBody() has been executed at least once?
	 */
	private boolean						doAfterBodyExecuted;

	/**
	 * Prepend application context to generated links.
	 */
	private boolean						dontAppendContext;

	/**
	 * Name of parameter which should not be forwarded during sorting or
	 * pagination.
	 */
	private String						excludedParams;

	/**
	 * add export links.
	 */
	private boolean						export;

	/**
	 * Included row range. If no rows can be skipped the range is from 0 to
	 * Long.MAX_VALUE. Range check should be always done using containsLong().
	 * This is an instance of org.apache.commons.lang.math.Range, but it's
	 * declared as Object to avoid runtime errors while Jasper tries to compile
	 * the page and commons lang 2.0 is not available. Commons lang version will
	 * be checked in the doStartTag() method in order to provide a more user
	 * friendly message.
	 */
	private Object						filteredRows;

	/**
	 * Static footer added using the footer tag.
	 */
	private String						footer;

	/**
	 * Is this the last iteration we will be performing? We only output the
	 * footer on the last iteration.
	 */
	private boolean						lastIteration;

	/**
	 * length of list to display.
	 */
	private int							length;

	/**
	 * Object (collection, list) on which the table is based. This is not set
	 * directly using a tag attribute and can be cleaned.
	 */
	protected Object					list;

	/**
	 * Object (collection, list) on which the table is based. Set directly using
	 * the "list" attribute or evaluated from expression.
	 */
	protected Object					listAttribute;

	/**
	 * Used by various functions when the person wants to do paging - cleaned in
	 * doEndTag().
	 */
	private SmartListHelper				listHelper;

	/**
	 * are we doing any local sorting? (defaults to True)
	 */
	private boolean						localSort						= true;

	/**
	 * name of the object to use for iteration. Can contain expressions.
	 */
	private String						name;

	// -- end tag attributes --

	/**
	 * list offset.
	 */
	private int							offset;

	/**
	 * page number - set in initParameters().
	 */
	private int							pageNumber						= 1;

	/**
	 * next row.
	 */

	/**
	 * page size.
	 */
	private int							pagesize;

	/**
	 * The paginated list containing the external pagination and sort parameters
	 * The presence of this paginated list is what determines if external
	 * pagination and sorting is used or not.
	 */
	private PaginatedList				paginatedList;

	/**
	 * The param encoder used to generate unique parameter names. Initialized at
	 * the first use of encodeParameter().
	 */
	private ParamEncoder				paramEncoder;

	/**
	 * list contains only viewable data.
	 */
	private boolean						partialList;

	/**
	 * table properties - set in doStartTag().
	 */
	private TableProperties				properties;

	/**
	 * Request uri.
	 */
	private String						requestUri;

	/**
	 * actual row number, updated during iteration.
	 */
	private int							rowNumber						= 1;

	/**
	 * Integer containing total size of the data displaytag is paginating
	 */
	private Object						size;

	/**
	 * Name of the Integer in some scope containing the size of the data
	 * displaytag is paginating
	 */
	private String						sizeObjectName;

	/**
	 * sort the full list?
	 */
	private Boolean						sortFullTable;

	/**
	 * Iterator on collection.
	 */
	private Iterator					tableIterator;

	/**
	 * table model - initialized in doStartTag().
	 */
	private TableModel					tableModel;

	/**
	 * Unique table id.
	 */
	private String						uid;

	/**
	 * The variable name to store totals in.
	 */
	private String						varTotals;

	/**
	 * Adds a cell to the current row. This method is usually called by a
	 * contained ColumnTag
	 * 
	 * @param cell
	 *            Cell to add to the current row
	 */
	public void addCell(Cell cell) {
		// check if null: could be null if list is empty, we don't need to fill
		// rows
		if (this.currentRow != null) {
			int columnNumber = this.currentRow.getCellList().size();
			this.currentRow.addCell(cell);

			// just be sure that the number of columns has not been altered by
			// conditionally including column tags in
			// different rows. This is not supported, but better avoid
			// IndexOutOfBounds...
			if (columnNumber < tableModel.getHeaderCellList().size()) {
				HeaderCell header = (HeaderCell) tableModel.getHeaderCellList()
						.get(columnNumber);
				header.addCell(new Column(header, cell, currentRow));
			}
		}
	}

	/**
	 * Called by interior column tags to help this tag figure out how it is
	 * supposed to display the information in the List it is supposed to
	 * display.
	 * 
	 * @param column
	 *            an internal tag describing a column in this tableview
	 */
	public void addColumn(HeaderCell column) {
		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] addColumn " + column);
		}

		if ((this.paginatedList != null) && (column.getSortable())) {
			String sortCriterion = paginatedList.getSortCriterion();

			String sortProperty = column.getSortProperty();
			if (sortProperty == null) {
				sortProperty = column.getBeanPropertyName();
			}

			if ((sortCriterion != null) && sortCriterion.equals(sortProperty)) {
				this.tableModel.setSortedColumnNumber(this.tableModel
						.getNumberOfColumns());
				column.setAlreadySorted();
			}
		}

		this.tableModel.addColumnHeader(column);
	}

	/**
	 * clean up instance variables, but not the ones representing tag
	 * attributes.
	 */
	private void cleanUp() {
		// reset instance variables (non attributes)
		this.currentMediaType = null;
		this.baseHref = null;
		this.caption = null;
		this.captionTag = null;
		this.currentRow = null;
		this.doAfterBodyExecuted = false;
		this.footer = null;
		this.listHelper = null;
		this.pageNumber = 0;
		this.paramEncoder = null;
		this.properties = null;
		this.rowNumber = 1;
		this.tableIterator = null;
		this.tableModel = null;
		this.list = null;
	}

	/**
	 * If no columns are provided, automatically add them from bean properties.
	 * Get the first object in the list and get all the properties (except the
	 * "class" property which is automatically skipped). Of course this isn't
	 * possible for empty lists.
	 */
	private void describeEmptyTable() {
		this.tableIterator = IteratorUtils.getIterator(this.list);

		if (this.tableIterator.hasNext()) {
			Object iteratedObject = this.tableIterator.next();
			Map objectProperties = new HashMap();

			// if it's a String don't add the "Bytes" column
			if (iteratedObject instanceof String) {
				return;
			}
			// if it's a map already use key names for column headers
			if (iteratedObject instanceof Map) {
				objectProperties = (Map) iteratedObject;
			} else {
				try {
					objectProperties = BeanUtils.describe(iteratedObject);
				} catch (Exception e) {
					log.warn("Unable to automatically add columns: "
							+ e.getMessage(), e);
				}
			}

			// iterator on properties names
			Iterator propertiesIterator = objectProperties.keySet().iterator();

			while (propertiesIterator.hasNext()) {
				// get the property name
				String propertyName = (String) propertiesIterator.next();

				// dont't want to add the standard "class" property
				if (!"class".equals(propertyName)) //$NON-NLS-1$
				{
					// creates a new header and add to the table model
					HeaderCell headerCell = new HeaderCell();
					headerCell.setBeanPropertyName(propertyName);

					// handle title i18n
					headerCell.setTitle(this.properties.geResourceProvider()
							.getResource(null, propertyName, this,
									this.pageContext));

					this.tableModel.addColumnHeader(headerCell);
				}
			}
		}
	}

	/**
	 * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody()
	 */
	public int doAfterBody() {
		// doAfterBody() has been called, body is not empty
		this.doAfterBodyExecuted = true;

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid()
					+ "] doAfterBody called - iterating on row "
					+ this.rowNumber);
		}

		// increment this.rowNumber
		this.rowNumber++;

		// Call doIteration() to do the common work
		return doIteration();
	}

	/**
	 * Draw the table. This is where everything happens, we figure out what
	 * values we are supposed to be showing, we figure out how we are supposed
	 * to be showing them, then we draw them.
	 * 
	 * @return int
	 * @throws JspException
	 *             generic exception
	 * @see javax.servlet.jsp.tagext.Tag#doEndTag()
	 */
	public int doEndTag() throws JspException {

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] doEndTag called");
		}

		if (!this.doAfterBodyExecuted) {
			if (log.isDebugEnabled()) {
				log.debug("[" + getUid() + "] tag body is empty.");
			}

			// first row (created in doStartTag)
			if (this.currentRow != null) {
				// if yes add to table model and remove
				this.tableModel.addRow(this.currentRow);
			}

			// other rows
			while (this.tableIterator.hasNext()) {
				Object iteratedObject = this.tableIterator.next();
				this.rowNumber++;

				// Row object for Cell values
				this.currentRow = new Row(iteratedObject, this.rowNumber);

				this.tableModel.addRow(this.currentRow);
			}
		}

		// if no rows are defined automatically get all properties from bean
		if (this.tableModel.isEmpty()) {
			describeEmptyTable();
		}

		// TableDecorator tableDecorator =
		// DecoratorFactory.loadTableDecorator(this.decoratorName);
		String tableDecoratorName = null;
		Object previousMediaType = this.pageContext
				.getAttribute(PAGE_ATTRIBUTE_MEDIA);
		if (MediaTypeEnum.HTML.equals(this.currentMediaType)
				&& (previousMediaType == null || MediaTypeEnum.HTML
						.equals(previousMediaType))) {
			tableDecoratorName = this.decoratorName;
		} else if (!MediaTypeEnum.HTML.equals(this.currentMediaType)) {
			tableDecoratorName = this.properties
					.getExportDecoratorName(this.currentMediaType);
		}
		TableDecorator tableDecorator = this.properties
				.getDecoratorFactoryInstance().loadTableDecorator(
						this.pageContext, tableDecoratorName);

		if (tableDecorator != null) {
			tableDecorator.init(this.pageContext, this.list, this.tableModel);
			this.tableModel.setTableDecorator(tableDecorator);
		}

		setupViewableData();

		// Figure out how we should sort this data, typically we just sort
		// the data being shown, but the programmer can override this behavior
		if (this.paginatedList == null && this.tableModel.isLocalSort()) {
			if (!this.tableModel.isSortFullTable()) {
				this.tableModel.sortPageList();
			}
		}

		// Get the data back in the representation that the user is after, do
		// they want HTML/XML/CSV/EXCEL/etc...
		int returnValue = EVAL_PAGE;

		// check for nested tables
		// Object previousMediaType =
		// this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
		if (MediaTypeEnum.HTML.equals(this.currentMediaType)
				&& (previousMediaType == null || MediaTypeEnum.HTML
						.equals(previousMediaType))) {
			writeHTMLData();
		} else if (!MediaTypeEnum.HTML.equals(this.currentMediaType)) {
			if (log.isDebugEnabled()) {
				log.debug("[" + getUid() + "] doEndTag - exporting");
			}

			returnValue = doExport();
		}

		// do not remove media attribute! if the table is nested in other tables
		// this is still needed
		// this.pageContext.removeAttribute(PAGE_ATTRIBUTE_MEDIA);

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] doEndTag - end");
		}

		cleanUp();
		return returnValue;
	}

	/**
	 * Called when data are not displayed in a html page but should be exported.
	 * 
	 * @return int SKIP_PAGE
	 * @throws JspException
	 *             generic exception
	 */
	protected int doExport() throws JspException {

		boolean exportFullList = this.properties.getExportFullList();

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] currentMediaType="
					+ this.currentMediaType);
		}

		boolean exportHeader = this.properties
				.getExportHeader(this.currentMediaType);
		boolean exportDecorated = this.properties.getExportDecorated();

		ExportView exportView = ExportViewFactory.getInstance().getView(
				this.currentMediaType, this.tableModel, exportFullList,
				exportHeader, exportDecorated);

		try {
			writeExport(exportView);
		} catch (IOException e) {
			throw new WrappedRuntimeException(getClass(), e);
		}

		return SKIP_PAGE;
	}

	/**
	 * Utility method that is used by both doStartTag() and doAfterBody() to
	 * perform an iteration.
	 * 
	 * @return <code>int</code> either EVAL_BODY_TAG or SKIP_BODY depending on
	 *         whether another iteration is desired.
	 */
	protected int doIteration() {

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] doIteration called");
		}

		// Row already filled?
		if (this.currentRow != null) {
			// if yes add to table model and remove
			this.tableModel.addRow(this.currentRow);
			this.currentRow = null;
		}

		if (this.tableIterator.hasNext()) {

			Object iteratedObject = this.tableIterator.next();
			if (getUid() != null) {
				if ((iteratedObject != null)) {
					// set object into this.pageContext
					if (log.isDebugEnabled()) {
						log.debug("[" + getUid() + "] setting attribute \""
								+ getUid() + "\" in pageContext");
					}
					this.pageContext.setAttribute(getUid(), iteratedObject);

				} else {
					// if row is null remove previous object
					this.pageContext.removeAttribute(getUid());
				}
				// set the current row number into this.pageContext
				this.pageContext.setAttribute(getUid()
						+ TableTagExtraInfo.ROWNUM_SUFFIX, Integer.valueOf(
						this.rowNumber));
			}

			// Row object for Cell values
			this.currentRow = new Row(iteratedObject, this.rowNumber);

			this.lastIteration = !this.tableIterator.hasNext();

			// new iteration
			// using int to avoid deprecation error in compilation using j2ee
			// 1.3
			return 2;
		}
		this.lastIteration = true;

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid()
					+ "] doIteration() - iterator ended after "
					+ (this.rowNumber - 1) + " rows");
		}

		// end iteration
		return SKIP_BODY;
	}

	/**
	 * When the tag starts, we just initialize some of our variables, and do a
	 * little bit of error checking to make sure that the user is not trying to
	 * give us parameters that we don't expect.
	 * 
	 * @return int
	 * @throws JspException
	 *             generic exception
	 * @see javax.servlet.jsp.tagext.Tag#doStartTag()
	 */
	public int doStartTag() throws JspException {
		DependencyChecker.check();

		// needed before column processing, elsewhere registered views will not
		// be added
		ExportViewFactory.getInstance();

		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] doStartTag called");
		}

		this.properties = TableProperties
				.getInstance((HttpServletRequest) pageContext.getRequest());
		this.tableModel = new TableModel(this.properties, pageContext
				.getResponse().getCharacterEncoding(), pageContext);

		// copying id to the table model for logging
		this.tableModel.setId(getUid());

		initParameters();

		this.tableModel.setMedia(this.currentMediaType);

		Object previousMediaType = this.pageContext
				.getAttribute(PAGE_ATTRIBUTE_MEDIA);
		// set the PAGE_ATTRIBUTE_MEDIA attribute in the page scope
		if (previousMediaType == null
				|| MediaTypeEnum.HTML.equals(previousMediaType)) {
			if (log.isDebugEnabled()) {
				log.debug("[" + getUid() + "] setting media ["
						+ this.currentMediaType + "] in this.pageContext");
			}
			this.pageContext.setAttribute(PAGE_ATTRIBUTE_MEDIA,
					this.currentMediaType);
		}

		doIteration();

		// always return EVAL_BODY_TAG to get column headers also if the table
		// is empty
		// using int to avoid deprecation error in compilation using j2ee 1.3
		return 2;
	}

	/**
	 * encode a parameter name to be unique in the page using ParamEncoder.
	 * 
	 * @param parameterName
	 *            parameter name to encode
	 * @return String encoded parameter name
	 */
	private String encodeParameter(String parameterName) {
		// paramEncoder has been already instantiated?
		if (this.paramEncoder == null) {
			// use the id attribute to get the unique identifier
			this.paramEncoder = new ParamEncoder(getUid());
		}

		return this.paramEncoder.encodeParameterName(parameterName);
	}

	/**
	 * Returns the base href with parameters. This is the instance used for
	 * links, need to be cloned before being modified.
	 * 
	 * @return base Href with parameters
	 */
	protected Href getBaseHref() {
		return this.baseHref;
	}

	/**
	 * Obtain the child caption tag.
	 * 
	 * @return The child caption tag
	 */
	public CaptionTag getCaptionTag() {
		return this.captionTag;
	}

	/**
	 * Create a complete string for compatibility with previous version before
	 * expression evaluation. This approach is optimized for new expressions,
	 * not for previous property/scope parameters.
	 * 
	 * @return Expression composed by scope + name + property
	 */
	private String getFullObjectName() {
		// only evaluate if needed, else preserve original list
		if (this.name == null) {
			return null;
		}

		return this.name;
	}

	/**
	 * Returns the name.
	 * 
	 * @return String
	 */
	protected String getName() {
		return this.name;
	}

	/**
	 * Returns the properties.
	 * 
	 * @return TableProperties
	 */
	protected TableProperties getProperties() {
		return this.properties;
	}

	/**
	 * Get the table model for this tag. Sometimes required by local tags that
	 * cooperate with DT. USE THIS METHOD WITH EXTREME CAUTION; IT PROVIDES
	 * ACCESS TO THE INTERNALS OF DISPLAYTAG, WHICH ARE NOT TO BE CONSIDERED
	 * STABLE PUBLIC INTERFACES.
	 * 
	 * @return the TableModel
	 */
	public TableModel getTableModel() {
		return this.tableModel;
	}

	/**
	 * Get the column totals Map. If there is no varTotals defined, there are no
	 * totals.
	 * 
	 * @return a Map of totals where the key is the column number and the value
	 *         is the total for that column
	 */
	public Map getTotals() {
		Map totalsMap = new HashMap();
		if (this.varTotals != null) {
			List headers = this.tableModel.getHeaderCellList();
			for (Iterator iterator = headers.iterator(); iterator.hasNext();) {
				HeaderCell headerCell = (HeaderCell) iterator.next();
				if (headerCell.isTotaled()) {
					totalsMap.put(
							"column" + (headerCell.getColumnNumber() + 1),
							new Double(headerCell.getTotal()));
				}
			}
		}
		return totalsMap;
	}

	/**
	 * Returns the unique id used to identify for this table.
	 * 
	 * @return id for this table
	 */
	public String getUid() {
		return this.uid;
	}

	/**
	 * Get the name that the totals should be stored under.
	 * 
	 * @return the var name in pageContext
	 */
	public String getVarTotals() {
		return this.varTotals;
	}

	/**
	 * init the href object used to generate all the links for pagination,
	 * sorting, exporting.
	 * 
	 * @param requestHelper
	 *            request helper used to extract the base Href
	 */
	protected void initHref(RequestHelper requestHelper) {
		// get the href for this request
		this.baseHref = requestHelper.getHref();

		if (this.excludedParams != null) {
			String[] splittedExcludedParams = StringUtils
					.split(this.excludedParams);

			// handle * keyword
			if (splittedExcludedParams.length == 1
					&& "*".equals(splittedExcludedParams[0])) {
				// @todo cleanup: paramEncoder initialization should not be done
				// here
				if (this.paramEncoder == null) {
					this.paramEncoder = new ParamEncoder(getUid());
				}

				Iterator paramsIterator = baseHref.getParameterMap().keySet()
						.iterator();
				while (paramsIterator.hasNext()) {
					String key = (String) paramsIterator.next();

					// don't remove parameters added by the table tag
					if (!this.paramEncoder.isParameterEncoded(key)) {
						baseHref.removeParameter(key);
					}
				}
			} else {
				for (int j = 0; j < splittedExcludedParams.length; j++) {
					baseHref.removeParameter(splittedExcludedParams[j]);
				}
			}
		}

		if (this.requestUri != null) {
			// if user has added a requestURI create a new href
			String fullURI = requestUri;
			if (!this.dontAppendContext) {
				String contextPath = ((HttpServletRequest) this.pageContext
						.getRequest()).getContextPath();

				// prepend the context path if any.
				// actually checks if context path is already there for people
				// which manually add it
				if (!StringUtils.isEmpty(contextPath) && requestUri != null
						&& requestUri.startsWith("/")
						&& !requestUri.startsWith(contextPath)) {
					fullURI = contextPath + this.requestUri;
				}
			}

			// call encodeURL to preserve session id when cookies are disabled
			fullURI = ((HttpServletResponse) this.pageContext.getResponse())
					.encodeURL(fullURI);

			baseHref.setFullUrl(fullURI);

			// // ... and copy parameters from the current request
			// Map parameterMap = normalHref.getParameterMap();
			// this.baseHref.addParameterMap(parameterMap);
		}

	}

	/**
	 * Reads parameters from the request and initialize all the needed table
	 * model attributes.
	 * 
	 * @throws FactoryInstantiationException
	 *             for problems in instantiating a RequestHelperFactory
	 */
	private void initParameters() throws JspTagException {

		if (rhf == null) {
			// first time initialization
			rhf = this.properties.getRequestHelperFactoryInstance();
		}

		String fullName = getFullObjectName();

		// only evaluate if needed, else use list attribute
		if (fullName != null) {
			this.list = evaluateExpression(fullName);
		} else if (this.list == null) {
			// needed to allow removing the collection of objects if not set
			// directly
			this.list = this.listAttribute;
		}

		if (this.list instanceof PaginatedList) {
			this.paginatedList = (PaginatedList) this.list;
			this.list = this.paginatedList.getList();
		}

		// set the table model to perform in memory local sorting
		this.tableModel.setLocalSort(this.localSort
				&& (this.paginatedList == null));

		RequestHelper requestHelper = rhf
				.getRequestHelperInstance(this.pageContext);

		initHref(requestHelper);

		Integer pageNumberParameter = requestHelper
				.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_PAGE));
		this.pageNumber = (pageNumberParameter == null) ? 1
				: pageNumberParameter.intValue();

		int sortColumn = -1;
		if (!this.tableModel.isLocalSort()) {
			// our sort column parameter may be a string, check that first
			String sortColumnName = requestHelper
					.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORT));

			// if usename is not null, sortColumnName is the name, if not is the
			// column index
			String usename = requestHelper
					.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORTUSINGNAME));

			if (sortColumnName == null) {
				this.tableModel.setSortedColumnNumber(this.defaultSortedColumn);
			} else {
				if (usename != null) {

					this.tableModel.setSortedColumnName(sortColumnName); // its
					// a
					// string,
					// set
					// as
					// string
				} else if (NumberUtils.isNumber(sortColumnName)) {
					sortColumn = Integer.parseInt(sortColumnName);
					this.tableModel.setSortedColumnNumber(sortColumn); // its
					// an
					// int
					// set
					// as
					// normal
				}
			}
		} else if (this.paginatedList == null) {
			Integer sortColumnParameter = requestHelper
					.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_SORT));
			sortColumn = (sortColumnParameter == null) ? this.defaultSortedColumn
					: sortColumnParameter.intValue();
			this.tableModel.setSortedColumnNumber(sortColumn);
		} else {
			sortColumn = defaultSortedColumn;
		}

		// default value
		boolean finalSortFull = this.properties.getSortFullList();

		// user value for this single table
		if (this.sortFullTable != null) {
			finalSortFull = this.sortFullTable.booleanValue();
		}

		// if a partial list is used and sort="list" is specified, assume the
		// partial list is already sorted
		if (!this.partialList || !finalSortFull) {
			this.tableModel.setSortFullTable(finalSortFull);
		}

		if (this.paginatedList == null) {
			SortOrderEnum paramOrder = SortOrderEnum
					.fromCode(requestHelper
							.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_ORDER)));

			// if no order parameter is set use default
			if (paramOrder == null) {
				paramOrder = this.defaultSortOrder;
			}

			boolean order = SortOrderEnum.DESCENDING != paramOrder;
			this.tableModel.setSortOrderAscending(order);
		} else {
			SortOrderEnum direction = paginatedList.getSortDirection();
			this.tableModel
					.setSortOrderAscending(direction == SortOrderEnum.ASCENDING);
		}

		Integer exportTypeParameter = requestHelper
				.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_EXPORTTYPE));

		this.currentMediaType = (MediaTypeEnum) ObjectUtils
				.defaultIfNull(MediaTypeEnum.fromCode(exportTypeParameter),
						MediaTypeEnum.HTML);

		// if we are doing partialLists then ensure we have our size object
		if (this.partialList) {
			if ((this.sizeObjectName == null) && (this.size == null)) {
				// ?
			}
			if (this.sizeObjectName != null) {
				// retrieve the object from scope
				this.size = evaluateExpression(this.sizeObjectName);
			}
			if (size == null) {
				throw new JspTagException(Messages.getString(
						"MissingAttributeException.msg",
						new Object[] { "size" }));
			} else if (!(size instanceof Integer)) {
				throw new JspTagException(Messages.getString(
						"InvalidTypeException.msg", new Object[] { "size",
								"Integer" }));
			}
		}

		// do we really need to skip any row?
		boolean wishOptimizedIteration = ((this.pagesize > 0 // we are paging
				|| this.offset > 0 // or we are skipping some records using
		// offset
		|| this.length > 0 // or we are limiting the records using length
		) && !partialList); // only optimize if we have the full list

		// can we actually skip any row?
		if (wishOptimizedIteration && (this.list instanceof Collection) // we
				// need
				// to
				// know
				// the
				// size
				&& ((sortColumn == -1 // and we are not sorting
				|| !finalSortFull // or we are sorting with the "page"
				// behaviour
				) && (this.currentMediaType == MediaTypeEnum.HTML // and we
				// are not
				// exporting
				|| !this.properties.getExportFullList()) // or we are
				// exporting a
				// single page
				)) {
			int start = 0;
			int end = 0;
			if (this.offset > 0) {
				start = this.offset;
			}
			if (length > 0) {
				end = start + this.length;
			}

			if (this.pagesize > 0) {
				int fullSize = ((Collection) this.list).size();
				start = (this.pageNumber - 1) * this.pagesize;

				// invalid page requested, go back to last page
				if (start > fullSize) {
					int div = fullSize / this.pagesize;
					start = (fullSize % this.pagesize == 0) ? div : div + 1;
				}

				end = start + this.pagesize;
			}

			// rowNumber starts from 1
			filteredRows = new LongRange(start + 1, end);
		} else {
			filteredRows = new LongRange(1, Long.MAX_VALUE);
		}

		this.tableIterator = IteratorUtils.getIterator(this.list);
	}

	/**
	 * Is the current row empty?
	 * 
	 * @return true if the current row is empty
	 */
	protected boolean isEmpty() {
		return this.currentRow == null;
	}

	/**
	 * Is this the first iteration?
	 * 
	 * @return boolean <code>true</code> if this is the first iteration
	 */
	protected boolean isFirstIteration() {
		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] first iteration="
					+ (this.rowNumber == 1) + " (row number=" + this.rowNumber
					+ ")");
		}
		// in first iteration this.rowNumber is 1
		// (this.rowNumber is incremented in doAfterBody)
		return this.rowNumber == 1;
	}

	/**
	 * Is the current row included in the "to-be-evaluated" range? Called by
	 * nested ColumnTags. If <code>false</code> column body is skipped.
	 * 
	 * @return <code>true</code> if the current row must be evaluated because
	 *         is included in output or because is included in sorting.
	 */
	protected boolean isIncludedRow() {
		return ((Range) filteredRows).containsLong(this.rowNumber);
	}

	/**
	 * Is this the last iteration?
	 * 
	 * @return boolean <code>true</code> if this is the last iteration
	 */
	protected boolean isLastIteration() {
		return this.lastIteration;
	}

	/**
	 * @see javax.servlet.jsp.tagext.Tag#release()
	 */
	public void release() {
		if (log.isDebugEnabled()) {
			log.debug("[" + getUid() + "] release() called");
		}

		super.release();

		// tag attributes
		this.decoratorName = null;
		this.defaultSortedColumn = -1;
		this.defaultSortOrder = null;
		this.export = false;
		this.length = 0;
		this.listAttribute = null;
		this.localSort = true;
		this.name = null;
		this.offset = 0;
		this.pagesize = 0;
		this.partialList = false;
		this.requestUri = null;
		this.dontAppendContext = false;
		this.sortFullTable = null;
		this.excludedParams = null;
		this.filteredRows = null;
		this.uid = null;
		this.paginatedList = null;
	}

	/**
	 * Sets the content of the caption. Called by a nested caption tag.
	 * 
	 * @param string
	 *            caption content
	 */
	public void setCaption(String string) {
		this.caption = string;
		this.tableModel.setCaption(this.caption);
	}

	/**
	 * Set the child caption tag.
	 * 
	 * @param captionTag
	 *            Child caption tag
	 */
	public void setCaptionTag(CaptionTag captionTag) {
		this.captionTag = captionTag;
	}

	/**
	 * Setter for the decorator class name.
	 * 
	 * @param decorator
	 *            fully qualified name of the table decorator to use
	 */
	public void setDecorator(String decorator) {
		this.decoratorName = decorator;
	}

	/**
	 * sets the sorting order for the sorted column.
	 * 
	 * @param value
	 *            "ascending" or "descending"
	 * @throws InvalidTagAttributeValueException
	 *             if value is not one of "ascending" or "descending"
	 */
	public void setDefaultorder(String value)
			throws InvalidTagAttributeValueException {
		this.defaultSortOrder = SortOrderEnum.fromName(value);
		if (this.defaultSortOrder == null) {
			throw new InvalidTagAttributeValueException(getClass(),
					"defaultorder", value); //$NON-NLS-1$
		}
	}

	/**
	 * sets the index of the default sorted column.
	 * 
	 * @param value
	 *            index of the column to sort
	 */
	public void setDefaultsort(int value) {
		// subtract one (internal index is 0 based)
		this.defaultSortedColumn = value - 1;
	}

	/**
	 * Sets the list of parameter which should not be forwarded during sorting
	 * or pagination.
	 * 
	 * @param value
	 *            whitespace separated list of parameters which should not be
	 *            included (* matches all parameters)
	 */
	public void setExcludedParams(String value) {
		this.excludedParams = value;
	}

	/**
	 * Is export enabled?
	 * 
	 * @param value
	 *            <code>true</code> if export should be enabled
	 */
	public void setExport(boolean value) {
		this.export = value;
	}

	/**
	 * Sets the content of the footer. Called by a nested footer tag.
	 * 
	 * @param string
	 *            footer content
	 */
	public void setFooter(String string) {
		this.footer = string;
		this.tableModel.setFooter(this.footer);
	}

	/**
	 * sets the number of items to be displayed in the page.
	 * 
	 * @param value
	 *            number of items to display in a page
	 */
	public void setLength(int value) {
		this.length = value;
	}

	/**
	 * Used to directly set a list (or any object you can iterate on).
	 * 
	 * @param value
	 *            Object
	 * @deprecated use setName() to get the object from the page or request
	 *             scope instead of setting it directly here
	 */
	public void setList(Object value) {
		this.listAttribute = value;
	}

	/**
	 * Sets the name of the object to use for iteration.
	 * 
	 * @param value
	 *            name of the object to use for iteration (can contain
	 *            expression). It also supports direct setting of a list, for
	 *            jsp 2.0 containers where users can set up a data source here
	 *            using EL expressions.
	 */
	public void setName(Object value) {
		if (value instanceof String) {
			// ok, assuming this is the name of the object
			this.name = (String) value;
		} else {
			// is this the list?
			this.list = value;
		}
	}

	/**
	 * Sets the name of the object to use for iteration. This setter is needed
	 * for jsp 1.1 container which doesn't support the String - Object
	 * conversion. The bean info class will swith to this setter.
	 * 
	 * @param value
	 *            name of the object
	 */
	public void setNameString(String value) {
		this.name = value;
	}

	/**
	 * Setter for the list offset attribute.
	 * 
	 * @param value
	 *            String
	 */
	public void setOffset(int value) {
		if (value < 1) {
			// negative values has no meaning, simply treat them as 0
			this.offset = 0;
		} else {
			this.offset = value - 1;
		}
	}

	/**
	 * sets the number of items that should be displayed for a single page.
	 * 
	 * @param value
	 *            number of items that should be displayed for a single page
	 */
	public void setPagesize(int value) {
		this.pagesize = value;
	}

	/**
	 * tells display tag that the values contained in the list are the viewable
	 * data only, there may be more results not given to displaytag
	 * 
	 * @param partialList
	 *            boolean value telling us there may be more data not given to
	 *            displaytag
	 */
	public void setPartialList(boolean partialList) {
		this.partialList = partialList;
	}

	/**
	 * Called by the setProperty tag to override some default behavior or text
	 * String.
	 * 
	 * @param propertyName
	 *            String property name
	 * @param propertyValue
	 *            String property value
	 */
	public void setProperty(String propertyName, String propertyValue) {
		this.properties.setProperty(propertyName, propertyValue);
	}

	/**
	 * setter for the "requestURI" attribute. Context path is automatically
	 * added to path starting with "/".
	 * 
	 * @param value
	 *            base URI for creating links
	 */
	public void setRequestURI(String value) {
		this.requestUri = value;
	}

	/**
	 * Setter for the "requestURIcontext" attribute.
	 * 
	 * @param value
	 *            base URI for creating links
	 */
	public void setRequestURIcontext(boolean value) {
		this.dontAppendContext = !value;
	}

	/**
	 * set the Integer containing the total size of the data displaytag is
	 * paginating
	 * 
	 * @param size
	 *            Integer containing the total size of the data
	 */
	public void setSize(Object size) {
		if (size instanceof String) {
			this.sizeObjectName = (String) size;
		} else {
			this.size = size;
		}
	}

	/**
	 * set the name of the Integer in some scope containing the total size of
	 * the data to be paginated
	 * 
	 * @param sizeObjectName
	 *            name of the Integer containing the total size of the data to
	 *            be paginated
	 */
	public void setSizeObjectName(String sizeObjectName) {
		this.sizeObjectName = sizeObjectName;
	}

	/**
	 * setter for the "sort" attribute.
	 * 
	 * @param value
	 *            "page" (sort a single page) or "list" (sort the full list)
	 * @throws InvalidTagAttributeValueException
	 *             if value is not "page" or "list"
	 */
	public void setSort(String value) throws InvalidTagAttributeValueException {
		if (TableTagParameters.SORT_AMOUNT_PAGE.equals(value)) {
			this.sortFullTable = Boolean.FALSE;
		} else if (TableTagParameters.SORT_AMOUNT_LIST.equals(value)) {
			this.sortFullTable = Boolean.TRUE;
		} else if (TableTagParameters.SORT_AMOUNT_EXTERNAL.equals(value)) {
			this.localSort = false;
		} else {
			throw new InvalidTagAttributeValueException(getClass(),
					"sort", value); //$NON-NLS-1$
		}
	}

	/**
	 * Sets the unique id used to identify for this table.
	 * 
	 * @param value
	 *            String
	 */
	public void setUid(String value) {
		this.uid = value;
	}

	/**
	 * This sets the list of all of the data that will be displayed on the page
	 * via the table tag. This might include just a subset of the total data in
	 * the list due to to paging being active, or the user asking us to just
	 * show a subset, etc...
	 */
	protected void setupViewableData() {

		// If the user has changed the way our default behavior works, then we
		// need to look for it now, and resort
		// things if needed before we ask for the viewable part. (this is a bad
		// place for this, this should be
		// refactored and moved somewhere else).

		if (this.paginatedList == null || this.tableModel.isLocalSort()) {
			if (this.tableModel.isSortFullTable()) {
				// Sort the total list...
				this.tableModel.sortFullList();
			}
		}

		Object originalData = this.tableModel.getRowListFull();

		// If they have asked for a subset of the list via the length
		// attribute, then only fetch those items out of the master list.
		List fullList = CollectionUtil.getListFromObject(originalData,
				this.offset, this.length);

		int pageOffset = this.offset;
		// If they have asked for just a page of the data, then use the
		// SmartListHelper to figure out what page they are after, etc...
		if (this.paginatedList == null && this.pagesize > 0) {
			this.listHelper = new SmartListHelper(fullList,
					(this.partialList) ? ((Integer) size).intValue() : fullList
							.size(), this.pagesize, this.properties,
					this.partialList);
			this.listHelper.setCurrentPage(this.pageNumber);
			pageOffset = this.listHelper.getFirstIndexForCurrentPage();
			fullList = this.listHelper.getListForCurrentPage();
		} else if (this.paginatedList != null) {
			this.listHelper = new PaginatedListSmartListHelper(
					this.paginatedList, this.properties);
		}
		this.tableModel.setRowListPage(fullList);
		this.tableModel.setPageOffset(pageOffset);
	}

	/**
	 * The variable name in which the totals map is stored.
	 * 
	 * @param varTotalsName
	 *            the value
	 */
	public void setVarTotals(String varTotalsName) {
		this.varTotals = varTotalsName;
	}

	/**
	 * Will write the export. The default behavior is to write directly to the
	 * response. If the ResponseOverrideFilter is configured for this request,
	 * will instead write the exported content to a map in the Request object.
	 * 
	 * @param exportView
	 *            export view
	 * @throws JspException
	 *             for problem in clearing the response or for invalid export
	 *             views
	 * @throws IOException
	 *             exception thrown when writing content to the response
	 */
	protected void writeExport(ExportView exportView) throws IOException,
			JspException {
		String filename = properties.getExportFileName(this.currentMediaType);

		HttpServletResponse response = (HttpServletResponse) this.pageContext
				.getResponse();
		HttpServletRequest request = (HttpServletRequest) this.pageContext
				.getRequest();
		// add the filename postfix for partition.
		if (request.getParameter("export-all") != null
				&& request.getParameter("PrmPagePart") != null) {
			String t_mail = filename.substring(0, filename.lastIndexOf("."));
			String t_postfix = filename.substring(filename.lastIndexOf("."));
			filename = t_mail + request.getParameter("PrmPagePart") + t_postfix;
		}

		Map bean = (Map) request.getAttribute(FILTER_CONTENT_OVERRIDE_BODY);
		boolean usingFilter = bean != null;

		String mimeType = exportView.getMimeType();
		// original encoding, be sure to add it back after reset()
		String characterEncoding = response.getCharacterEncoding();

		if (usingFilter) {
			if (!bean.containsKey(TableTagParameters.BEAN_BUFFER)) {
				// We are running under the export filter, call it
				log
						.debug("Exportfilter enabled in unbuffered mode, setting headers");
				response.addHeader(TableTagParameters.PARAMETER_EXPORTING,
						TagConstants.EMPTY_STRING);
			} else {
				// We are running under the export filter in buffered mode
				bean.put(TableTagParameters.BEAN_CONTENTTYPE, mimeType);
				bean.put(TableTagParameters.BEAN_FILENAME, filename);

				if (exportView instanceof TextExportView) {
					StringWriter writer = new StringWriter();
					((TextExportView) exportView).doExport(writer);
					bean.put(TableTagParameters.BEAN_BODY, writer.toString());
				} else if (exportView instanceof BinaryExportView) {
					ByteArrayOutputStream stream = new ByteArrayOutputStream();
					((BinaryExportView) exportView).doExport(stream);
					bean
							.put(TableTagParameters.BEAN_BODY, stream
									.toByteArray());

				} else {
					throw new JspTagException(
							"Export view "
									+ exportView.getClass().getName()
									+ " must implement TextExportView or BinaryExportView");
				}

				return;
			}
		} else {
			log.debug("Exportfilter NOT enabled");
			// response can't be already committed at this time
			if (response.isCommitted()) {
				throw new ExportException(getClass());
			}

			try {
				response.reset();
				pageContext.getOut().clearBuffer();
			} catch (Exception e) {
				throw new ExportException(getClass());
			}
		}

		if (!usingFilter && characterEncoding != null
				&& mimeType.indexOf("charset") == -1) //$NON-NLS-1$
		{
			mimeType += "; charset=" + characterEncoding; //$NON-NLS-1$
		}

		response.setContentType(mimeType);

		if (StringUtils.isNotEmpty(filename)) {
			response.setHeader("Content-Disposition", //$NON-NLS-1$
					"attachment; filename=\"" + filename + "\""); //$NON-NLS-1$ //$NON-NLS-2$
		}

		if (exportView instanceof TextExportView) {
			Writer writer;
			if (usingFilter) {
				writer = response.getWriter();
			} else {
				writer = pageContext.getOut();
			}

			((TextExportView) exportView).doExport(writer);
		} else if (exportView instanceof BinaryExportView) {
			// dealing with binary content
			// note that this is not assured to work on any application server
			// if the filter is not enabled. According
			// to the jsp specs response.getOutputStream() should no be called
			// in jsps.
			((BinaryExportView) exportView)
					.doExport(response.getOutputStream());
		} else {
			throw new JspTagException("Export view "
					+ exportView.getClass().getName()
					+ " must implement TextExportView or BinaryExportView");
		}

		log.debug("Export completed");

	}

	/**
	 * Uses HtmlTableWriter to write table called when data have to be displayed
	 * in a html page.
	 * 
	 * @throws JspException
	 *             generic exception
	 */
	private void writeHTMLData() throws JspException {
		JspWriter out = this.pageContext.getOut();

		String css = this.properties.getCssTable();
		if (StringUtils.isNotBlank(css)) {
			this.addClass(css);
		}
		// use HtmlTableWriter to write table
		new HtmlTableWriter(this.tableModel, this.properties, this.baseHref,
				this.export, out, getCaptionTag(), this.paginatedList,
				this.listHelper, this.pagesize, getAttributeMap(), this.uid)
				.writeTable(this.tableModel, this.getUid());

		if (this.varTotals != null) {
			pageContext.setAttribute(this.varTotals, getTotals());
		}
	}

}